#region Namespaces

using System;
using System.Xml;

#endregion

namespace Vibstudio.X4NET.Xml.Linq
{
    internal class XNodeWriter : XmlWriter
    {
        private readonly XContainer _root;

        private XAttribute _attribute;

        private XContainer _current;

        private bool _isClosed;

        private XmlNodeType _state;

        public XNodeWriter(XContainer fragment)
        {
            _root = fragment;
            _state = XmlNodeType.None;
            _current = fragment;
        }

        public override WriteState WriteState
        {
            get
            {
                if (_isClosed)
                {
                    return WriteState.Closed;
                }
                if (_attribute != null)
                {
                    return WriteState.Attribute;
                }

                switch (_state)
                {
                    case XmlNodeType.None:
                        return WriteState.Start;
                    case XmlNodeType.XmlDeclaration:
                        return WriteState.Prolog;
                    case XmlNodeType.DocumentType:
                        return WriteState.Element;
                    default:
                        return WriteState.Content;
                }
            }
        }

        private void CheckState()
        {
            if (_isClosed)
            {
                throw new InvalidOperationException();
            }
        }

        private void WritePossiblyTopLevelNode(XNode n, bool possiblyAttribute)
        {
            CheckState();
            if (!possiblyAttribute && _attribute != null)
            {
                throw new InvalidOperationException(String.Format("Current state is not acceptable for {0}.", n.NodeType));
            }

            if (_state != XmlNodeType.Element)
            {
                _root.Add(n);
            }
            else if (_attribute != null)
            {
                _attribute.Value += XUtil.ToString(n);
            }
            else
            {
                _current.Add(n);
            }
            if (_state == XmlNodeType.None)
            {
                _state = XmlNodeType.XmlDeclaration;
            }
        }

        private void FillXmlns(XElement el, string prefix, XNamespace xns)
        {
            if (xns == XNamespace.Xmlns)
            {
                return;
            }
            if (prefix == null)
            {
                return;
            }

            if (xns == XNamespace.None)
            {
                var xnsPrefix = el.GetPrefixOfNamespace(xns);
                if (xnsPrefix != null && xnsPrefix != prefix)
                {
                    el.SetAttributeValue(prefix == string.Empty ? XNamespace.None.GetName("xmlns") : XNamespace.Xmlns.GetName(prefix),
                                         xns.NamespaceName);
                }
            }
            else if (el.GetDefaultNamespace() != XNamespace.None)
            {
                el.SetAttributeValue(XNamespace.None.GetName("xmlns"), xns.NamespaceName);
            }
        }

        public override void Close()
        {
            CheckState();
            _isClosed = true;
        }

        public override void Flush()
        {
        }

        public override string LookupPrefix(string ns)
        {
            CheckState();
            if (_current == null)
            {
                throw new InvalidOperationException();
            }
            XElement el = (_current as XElement) ?? _current.Parent;

            if (ns == el.Name.Namespace.NamespaceName)
            {
                return "";
            }

            return el != null ? el.GetPrefixOfNamespace(XNamespace.Get(ns)) : null;
        }

        public override void WriteStartDocument()
        {
            WriteStartDocument(null);
        }

        public override void WriteStartDocument(bool standalone)
        {
            WriteStartDocument(standalone ? "yes" : "no");
        }

        private void WriteStartDocument(string sddecl)
        {
            CheckState();
            if (_state != XmlNodeType.None)
            {
                throw new InvalidOperationException("Current state is not acceptable for xmldecl.");
            }
            XDocument doc = _current as XDocument;
            if (doc == null)
            {
                throw new InvalidOperationException("Only document node can accept xml declaration");
            }

            doc.Declaration = new XDeclaration("1.0", null, sddecl);
            _state = XmlNodeType.XmlDeclaration;
        }

        public override void WriteEndDocument()
        {
            CheckState();

            _isClosed = true;
        }

        public override void WriteDocType(string name, string publicId, string systemId, string internalSubset)
        {
            CheckState();
            switch (_state)
            {
                case XmlNodeType.None:
                case XmlNodeType.XmlDeclaration:
                    XDocument doc = _current as XDocument;
                    if (doc == null)
                    {
                        throw new InvalidOperationException("Only document node can accept doctype declaration");
                    }
                    doc.Add(new XDocumentType(name, publicId, systemId, internalSubset));
                    _state = XmlNodeType.DocumentType;
                    break;
                default:
                    throw new InvalidOperationException("Current state is not acceptable for doctype.");
            }
        }

        public override void WriteStartElement(string prefix, string name, string ns)
        {
            CheckState();

            XNamespace xns = XNamespace.Get(ns ?? String.Empty);
            XElement el = new XElement(xns.GetName(name));
            if (_current == null)
            {
                _root.Add(el);
                _state = XmlNodeType.Element;
            }
            else
            {
                _current.Add(el);
                _state = XmlNodeType.Element;
            }

            FillXmlns(el, prefix, xns);

            _current = el;
        }

        public override void WriteEndElement()
        {
            WriteEndElementInternal(false);
        }

        public override void WriteFullEndElement()
        {
            WriteEndElementInternal(true);
        }

        private void WriteEndElementInternal(bool forceFull)
        {
            CheckState();
            if (_current == null)
            {
                throw new InvalidOperationException("Current state is not acceptable for endElement.");
            }

            XElement el = _current as XElement;
            if (forceFull)
            {
                if (el != null)
                {
                    el.IsEmpty = false;
                }
            }

            _current = _current.Parent;
        }

        public override void WriteStartAttribute(string prefix, string name, string ns)
        {
            CheckState();
            if (_attribute != null)
            {
                throw new InvalidOperationException("There is an open attribute.");
            }
            XElement el = _current as XElement;
            if (el == null)
            {
                throw new InvalidOperationException("Current state is not acceptable for startAttribute.");
            }
            if (prefix == null)
            {
                prefix = String.Empty;
            }

            if (prefix.Length == 0 && name == "xmlns" && ns == XNamespace.Xmlns.NamespaceName)
            {
                ns = String.Empty;
            }

            XNamespace xns = ns == null ? XNamespace.None : XNamespace.Get(ns);
            el.SetAttributeValue(xns.GetName(name), String.Empty);
            _attribute = el.LastAttribute;
            FillXmlns(el, ns != null ? prefix : null, xns);
        }

        public override void WriteEndAttribute()
        {
            CheckState();
            if (_attribute == null)
            {
                throw new InvalidOperationException("Current state is not acceptable for startAttribute.");
            }

            _attribute = null;
        }

        public override void WriteCData(string data)
        {
            CheckState();
            if (_current == null)
            {
                throw new InvalidOperationException("Current state is not acceptable for CDATAsection.");
            }

            _current.Add(new XCData(data));
        }

        public override void WriteComment(string comment)
        {
            WritePossiblyTopLevelNode(new XComment(comment), false);
        }

        public override void WriteProcessingInstruction(string name, string value)
        {
            WritePossiblyTopLevelNode(
                                      new XProcessingInstruction(name, value),
                                      false);
        }

        public override void WriteEntityRef(string name)
        {
            throw new NotSupportedException();
        }

        public override void WriteCharEntity(char c)
        {
            throw new NotSupportedException();
        }

        public override void WriteWhitespace(string ws)
        {
            WritePossiblyTopLevelNode(new XText(ws), true);
        }

        public override void WriteString(string data)
        {
            CheckState();
            if (_current == null)
            {
                throw new InvalidOperationException("Current state is not acceptable for Text.");
            }

            if (_attribute != null)
            {
                _attribute.Value += data;
            }
            else
            {
                _current.Add(data);
            }
        }

        public override void WriteName(string name)
        {
            WriteString(name);
        }

        public override void WriteNmToken(string nmtoken)
        {
            WriteString(nmtoken);
        }

        public override void WriteQualifiedName(string name, string ns)
        {
            string prefix = LookupPrefix(ns);
            if (prefix == null)
            {
                throw new ArgumentException(String.Format("Invalid namespace {0}", ns));
            }
            if (prefix != String.Empty)
            {
                WriteString(name);
            }
            else
            {
                WriteString(prefix + ":" + name);
            }
        }

        public override void WriteChars(char[] chars, int start, int len)
        {
            WriteString(new string(chars, start, len));
        }

        public override void WriteRaw(string data)
        {
            WriteString(data);
        }

        public override void WriteRaw(char[] chars, int start, int len)
        {
            WriteChars(chars, start, len);
        }

        public override void WriteBase64(byte[] data, int start, int len)
        {
            WriteString(Convert.ToBase64String(data, start, len));
        }

        public override void WriteBinHex(byte[] data, int start, int len)
        {
            throw new NotImplementedException();
        }

        public override void WriteSurrogateCharEntity(char c1, char c2)
        {
            throw new NotImplementedException();
        }
    }
}